/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.validator.internal;

import java.util.*;
import ch.simpleel.util.*;
import ch.trackedbean.validator.*;

/**
 * Object holding contextual information used during an object structure validation.
 * 
 * @author M. Hautle
 */
public class ValidationContext implements IValidationContext {
    /** The result object. */
    protected final ValidationResult result = new ValidationResult();

    /** Holds the current el path. */
    protected String currentPath = "";

    /** Set holding the already visited objects. */
    protected final IdentityHashMap<Object, Boolean> visited = new IdentityHashMap<Object, Boolean>();

    /** Map for context dependent values. Default is {@link Collections#EMPTY_MAP} cause only few calls will use this feature. */
    protected Map<Object, Object> config = Collections.emptyMap();

    /**
     * Default constructor.
     * 
     * @param root The root element of the structure to validate
     */
    public ValidationContext(Object root) {
        visited.put(root, Boolean.TRUE);
    }

    /**
     * {@inheritDoc}
     */
    public boolean stepIntoObject(String elementName, Object element) {
        // set the visited flag, if the element was allready visited we just return
        if (visited.put(element, Boolean.TRUE) != null)
            return false;
        currentPath = getResultingPath(elementName);
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public void stepOutOfObject(Object element, IErrorDescription error) {
        if (error != null)
            result.addErrors(currentPath, error);
        final int index = currentPath.lastIndexOf('.');
        // take care when we return to the root (there's no '.' at the start)
        currentPath = currentPath.substring(0, index > -1 ? index : 0);
    }

    /**
     * {@inheritDoc}
     */
    public boolean stepIntoCollectionElement(int index, Object element) {
        if (visited.put(element, Boolean.TRUE) != null)
            return false;
        currentPath = new StringBuilder(currentPath).append('[').append(index).append(']').toString();
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public void stepOutOfCollectionElement() {
        currentPath = currentPath.substring(0, currentPath.lastIndexOf('['));
    }

    /**
     * {@inheritDoc}
     */
    public boolean stepIntoMapElement(Object key, Object element) {
        if (visited.put(element, Boolean.TRUE) != null)
            return false;
        currentPath = new StringBuilder(currentPath).append("['").append(key).append("']").toString();
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public void stepOutOfMapElement() {
        currentPath = currentPath.substring(0, currentPath.lastIndexOf('['));
    }

    /**
     * {@inheritDoc}
     */
    public boolean addGeneralError(IErrorDescription error) {
        if (error == null)
            return false;
        result.addErrors(currentPath.length() == 0 ? ValidationResult.GENERAL : currentPath, new IErrorDescription[] { error });
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public boolean addErrors(String property, IErrorDescription... errors) {
        if (errors == null)
            return false;
        assert ELHelper.isValid(property) : "Property expression " + property + " is invalid!";
        result.addErrors(getResultingPath(property), errors);
        return true;
    }

    /**
     * Appends the given element to {@link #currentPath} and returns it.
     * 
     * @param element The element to add
     * @return The resulting path
     */
    protected String getResultingPath(String element) {
        final StringBuilder path = new StringBuilder(currentPath);
        // the first step down shouldn't have a '.' before the name
        if (path.length() > 0)
            path.append('.');
        return path.append(element).toString();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings("unchecked")
    public <T> T getValue(Object key) {
        return (T) config.get(key);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object putValue(Object key, Object value) {
        // empty == Collections.EMPTY_MAP; so we create the real config map (this is a kind of 'lazy' creation)
        if (config.isEmpty())
            config = new HashMap<Object, Object>();
        return config.put(key, value);
    }

    /**
     * {@inheritDoc}
     */
    public ValidationResult getResult() {
        return result;
    }
}
